/********************************************************************* * * Copyright (C) 2002 Andrew Khan * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ***************************************************************************/ package jxl.read.biff; import java.util.ArrayList; import java.util.Iterator; import java.util.regex.Pattern; import jxl.common.Logger; import jxl.common.Assert; import jxl.Cell; import jxl.CellType; import jxl.CellView; import jxl.Hyperlink; import jxl.Image; import jxl.LabelCell; import jxl.Range; import jxl.Sheet; import jxl.SheetSettings; import jxl.WorkbookSettings; import jxl.biff.BuiltInName; import jxl.biff.AutoFilter; import jxl.biff.CellFinder; import jxl.biff.CellReferenceHelper; import jxl.biff.ConditionalFormat; import jxl.biff.DataValidation; import jxl.biff.EmptyCell; import jxl.biff.FormattingRecords; import jxl.biff.Type; import jxl.biff.WorkspaceInformationRecord; import jxl.biff.drawing.Chart; import jxl.biff.drawing.Drawing; import jxl.biff.drawing.DrawingData; import jxl.biff.drawing.DrawingGroupObject; import jxl.format.CellFormat; /** * Represents a sheet within a workbook. Provides a handle to the individual * cells, or lines of cells (grouped by Row or Column) * In order to simplify this class due to code bloat, the actual reading * logic has been delegated to the SheetReaderClass. This class' main * responsibility is now to implement the API methods declared in the * Sheet interface */ public class SheetImpl implements Sheet { /** * The logger */ private static Logger logger = Logger.getLogger(SheetImpl.class); /** * The excel file */ private File excelFile; /** * A handle to the shared string table */ private SSTRecord sharedStrings; /** * A handle to the sheet BOF record, which indicates the stream type */ private BOFRecord sheetBof; /** * A handle to the workbook BOF record, which indicates the stream type */ private BOFRecord workbookBof; /** * A handle to the formatting records */ private FormattingRecords formattingRecords; /** * The name of this sheet */ private String name; /** * The number of rows */ private int numRows; /** * The number of columns */ private int numCols; /** * The cells */ private Cell[][] cells; /** * The start position in the stream of this sheet */ private int startPosition; /** * The list of specified (ie. non default) column widths */ private ColumnInfoRecord[] columnInfos; /** * The array of row records */ private RowRecord[] rowRecords; /** * The list of non-default row properties */ private ArrayList rowProperties; /** * An array of column info records. They are held this way before * they are transferred to the more convenient array */ private ArrayList columnInfosArray; /** * A list of shared formula groups */ private ArrayList sharedFormulas; /** * A list of hyperlinks on this page */ private ArrayList hyperlinks; /** * A list of charts on this page */ private ArrayList charts; /** * A list of drawings on this page */ private ArrayList drawings; /** * A list of drawings (as opposed to comments/validation/charts) on this * page */ private ArrayList images; /** * A list of data validations on this page */ private DataValidation dataValidation; /** * A list of merged cells on this page */ private Range[] mergedCells; /** * Indicates whether the columnInfos array has been initialized */ private boolean columnInfosInitialized; /** * Indicates whether the rowRecords array has been initialized */ private boolean rowRecordsInitialized; /** * Indicates whether or not the dates are based around the 1904 date system */ private boolean nineteenFour; /** * The workspace options */ private WorkspaceInformationRecord workspaceOptions; /** * The hidden flag */ private boolean hidden; /** * The environment specific print record */ private PLSRecord plsRecord; /** * The property set record associated with this workbook */ private ButtonPropertySetRecord buttonPropertySet; /** * The sheet settings */ private SheetSettings settings; /** * The horizontal page breaks contained on this sheet */ private int[] rowBreaks; /** * The vertical page breaks contained on this sheet */ private int[] columnBreaks; /** * The maximum row outline level */ private int maxRowOutlineLevel; /** * The maximum column outline level */ private int maxColumnOutlineLevel; /** * The list of local names for this sheet */ private ArrayList localNames; /** * The list of conditional formats for this sheet */ private ArrayList conditionalFormats; /** * The autofilter information */ private AutoFilter autoFilter; /** * A handle to the workbook which contains this sheet. Some of the records * need this in order to reference external sheets */ private WorkbookParser workbook; /** * A handle to the workbook settings */ private WorkbookSettings workbookSettings; /** * Constructor * * @param f the excel file * @param sst the shared string table * @param fr formatting records * @param sb the bof record which indicates the start of the sheet * @param wb the bof record which indicates the start of the sheet * @param nf the 1904 flag * @param wp the workbook which this sheet belongs to * @exception BiffException */ SheetImpl(File f, SSTRecord sst, FormattingRecords fr, BOFRecord sb, BOFRecord wb, boolean nf, WorkbookParser wp) throws BiffException { excelFile = f; sharedStrings = sst; formattingRecords = fr; sheetBof = sb; workbookBof = wb; columnInfosArray = new ArrayList(); sharedFormulas = new ArrayList(); hyperlinks = new ArrayList(); rowProperties = new ArrayList(10); columnInfosInitialized = false; rowRecordsInitialized = false; nineteenFour = nf; workbook = wp; workbookSettings = workbook.getSettings(); // Mark the position in the stream, and then skip on until the end startPosition = f.getPos(); if (sheetBof.isChart()) { // Set the start pos to include the bof so the sheet reader can handle it startPosition -= (sheetBof.getLength() + 4); } Record r = null; int bofs = 1; while (bofs >= 1) { r = f.next(); // use this form for quick performance if (r.getCode() == Type.EOF.value) { bofs--; } if (r.getCode() == Type.BOF.value) { bofs++; } } } /** * Returns the cell for the specified location eg. "A4", using the * CellReferenceHelper * * @param loc the cell reference * @return the cell at the specified co-ordinates */ public Cell getCell(String loc) { return getCell(CellReferenceHelper.getColumn(loc), CellReferenceHelper.getRow(loc)); } /** * Returns the cell specified at this row and at this column * * @param row the row number * @param column the column number * @return the cell at the specified co-ordinates */ public Cell getCell(int column, int row) { // just in case this has been cleared, but something else holds // a reference to it if (cells == null) { readSheet(); } Cell c = cells[row][column]; if (c == null) { c = new EmptyCell(column, row); cells[row][column] = c; } return c; } /** * Gets the cell whose contents match the string passed in. * If no match is found, then null is returned. The search is performed * on a row by row basis, so the lower the row number, the more * efficiently the algorithm will perform * * @param contents the string to match * @return the Cell whose contents match the paramter, null if not found */ public Cell findCell(String contents) { CellFinder cellFinder = new CellFinder(this); return cellFinder.findCell(contents); } /** * Gets the cell whose contents match the string passed in. * If no match is found, then null is returned. The search is performed * on a row by row basis, so the lower the row number, the more * efficiently the algorithm will perform * * @param contents the string to match * @param firstCol the first column within the range * @param firstRow the first row of the range * @param lastCol the last column within the range * @param lastRow the last row within the range * @param reverse indicates whether to perform a reverse search or not * @return the Cell whose contents match the parameter, null if not found */ public Cell findCell(String contents, int firstCol, int firstRow, int lastCol, int lastRow, boolean reverse) { CellFinder cellFinder = new CellFinder(this); return cellFinder.findCell(contents, firstCol, firstRow, lastCol, lastRow, reverse); } /** * Gets the cell whose contents match the regular expressionstring passed in. * If no match is found, then null is returned. The search is performed * on a row by row basis, so the lower the row number, the more * efficiently the algorithm will perform * * @param pattern the regular expression string to match * @param firstCol the first column within the range * @param firstRow the first row of the range * @param lastRow the last row within the range * @param lastCol the last column within the ranage * @param reverse indicates whether to perform a reverse search or not * @return the Cell whose contents match the parameter, null if not found */ public Cell findCell(Pattern pattern, int firstCol, int firstRow, int lastCol, int lastRow, boolean reverse) { CellFinder cellFinder = new CellFinder(this); return cellFinder.findCell(pattern, firstCol, firstRow, lastCol, lastRow, reverse); } /** * Gets the cell whose contents match the string passed in. * If no match is found, then null is returned. The search is performed * on a row by row basis, so the lower the row number, the more * efficiently the algorithm will perform. This method differs * from the findCell methods in that only cells with labels are * queried - all numerical cells are ignored. This should therefore * improve performance. * * @param contents the string to match * @return the Cell whose contents match the paramter, null if not found */ public LabelCell findLabelCell(String contents) { CellFinder cellFinder = new CellFinder(this); return cellFinder.findLabelCell(contents); } /** * Returns the number of rows in this sheet * * @return the number of rows in this sheet */ public int getRows() { // just in case this has been cleared, but something else holds // a reference to it if (cells == null) { readSheet(); } return numRows; } /** * Returns the number of columns in this sheet * * @return the number of columns in this sheet */ public int getColumns() { // just in case this has been cleared, but something else holds // a reference to it if (cells == null) { readSheet(); } return numCols; } /** * Gets all the cells on the specified row. The returned array will * be stripped of all trailing empty cells * * @param row the rows whose cells are to be returned * @return the cells on the given row */ public Cell[] getRow(int row) { // just in case this has been cleared, but something else holds // a reference to it if (cells == null) { readSheet(); } // Find the last non-null cell boolean found = false; int col = numCols - 1; while (col >= 0 && !found) { if (cells[row][col] != null) { found = true; } else { col--; } } // Only create entries for non-null cells Cell[] c = new Cell[col + 1]; for (int i = 0; i <= col; i++) { c[i] = getCell(i, row); } return c; } /** * Gets all the cells on the specified column. The returned array * will be stripped of all trailing empty cells * * @param col the column whose cells are to be returned * @return the cells on the specified column */ public Cell[] getColumn(int col) { // just in case this has been cleared, but something else holds // a reference to it if (cells == null) { readSheet(); } // Find the last non-null cell boolean found = false; int row = numRows - 1; while (row >= 0 && !found) { if (cells[row][col] != null) { found = true; } else { row--; } } // Only create entries for non-null cells Cell[] c = new Cell[row + 1]; for (int i = 0; i <= row; i++) { c[i] = getCell(col, i); } return c; } /** * Gets the name of this sheet * * @return the name of the sheet */ public String getName() { return name; } /** * Sets the name of this sheet * * @param s the sheet name */ final void setName(String s) { name = s; } /** * Determines whether the sheet is hidden * * @return whether or not the sheet is hidden * @deprecated in favour of the getSettings function */ public boolean isHidden() { return hidden; } /** * Gets the column info record for the specified column. If no * column is specified, null is returned * * @param col the column * @return the ColumnInfoRecord if specified, NULL otherwise */ public ColumnInfoRecord getColumnInfo(int col) { if (!columnInfosInitialized) { // Initialize the array Iterator i = columnInfosArray.iterator(); ColumnInfoRecord cir = null; while (i.hasNext()) { cir = (ColumnInfoRecord) i.next(); int startcol = Math.max(0, cir.getStartColumn()); int endcol = Math.min(columnInfos.length - 1, cir.getEndColumn()); for (int c = startcol; c <= endcol; c++) { columnInfos[c] = cir; } if (endcol < startcol) { columnInfos[startcol] = cir; } } columnInfosInitialized = true; } return col < columnInfos.length ? columnInfos[col] : null; } /** * Gets all the column info records * * @return the ColumnInfoRecordArray */ public ColumnInfoRecord[] getColumnInfos() { // Just chuck all the column infos we have into an array ColumnInfoRecord[] infos = new ColumnInfoRecord[columnInfosArray.size()]; for (int i = 0; i < columnInfosArray.size(); i++) { infos[i] = (ColumnInfoRecord) columnInfosArray.get(i); } return infos; } /** * Sets the visibility of this sheet * * @param h hidden flag */ final void setHidden(boolean h) { hidden = h; } /** * Clears out the array of cells. This is done for memory allocation * reasons when reading very large sheets */ final void clear() { cells = null; mergedCells = null; columnInfosArray.clear(); sharedFormulas.clear(); hyperlinks.clear(); columnInfosInitialized = false; if (!workbookSettings.getGCDisabled()) { System.gc(); } } /** * Reads in the contents of this sheet */ final void readSheet() { // If this sheet contains only a chart, then set everything to // empty and do not bother parsing the sheet // Thanks to steve.brophy for spotting this if (!sheetBof.isWorksheet()) { numRows = 0; numCols = 0; cells = new Cell[0][0]; // return; } SheetReader reader = new SheetReader(excelFile, sharedStrings, formattingRecords, sheetBof, workbookBof, nineteenFour, workbook, startPosition, this); reader.read(); // Take stuff that was read in numRows = reader.getNumRows(); numCols = reader.getNumCols(); cells = reader.getCells(); rowProperties = reader.getRowProperties(); columnInfosArray = reader.getColumnInfosArray(); hyperlinks = reader.getHyperlinks(); conditionalFormats = reader.getConditionalFormats(); autoFilter = reader.getAutoFilter(); charts = reader.getCharts(); drawings = reader.getDrawings(); dataValidation = reader.getDataValidation(); mergedCells = reader.getMergedCells(); settings = reader.getSettings(); settings.setHidden(hidden); rowBreaks = reader.getRowBreaks(); columnBreaks = reader.getColumnBreaks(); workspaceOptions = reader.getWorkspaceOptions(); plsRecord = reader.getPLS(); buttonPropertySet = reader.getButtonPropertySet(); maxRowOutlineLevel = reader.getMaxRowOutlineLevel(); maxColumnOutlineLevel = reader.getMaxColumnOutlineLevel(); reader = null; if (!workbookSettings.getGCDisabled()) { System.gc(); } if (columnInfosArray.size() > 0) { ColumnInfoRecord cir = (ColumnInfoRecord) columnInfosArray.get(columnInfosArray.size() - 1); columnInfos = new ColumnInfoRecord[cir.getEndColumn() + 1]; } else { columnInfos = new ColumnInfoRecord[0]; } // Add any local names if (localNames != null) { for (Iterator it = localNames.iterator(); it.hasNext() ;) { NameRecord nr = (NameRecord) it.next(); if (nr.getBuiltInName() == BuiltInName.PRINT_AREA) { if(nr.getRanges().length > 0) { NameRecord.NameRange rng = nr.getRanges()[0]; settings.setPrintArea(rng.getFirstColumn(), rng.getFirstRow(), rng.getLastColumn(), rng.getLastRow()); } } else if (nr.getBuiltInName() == BuiltInName.PRINT_TITLES) { // There can be 1 or 2 entries. // Row entries have hardwired column entries (first and last // possible column) // Column entries have hardwired row entries (first and last // possible row) for (int i = 0 ; i < nr.getRanges().length ; i++) { NameRecord.NameRange rng = nr.getRanges()[i]; if (rng.getFirstColumn() == 0 && rng.getLastColumn() == 255) { settings.setPrintTitlesRow(rng.getFirstRow(), rng.getLastRow()); } else { settings.setPrintTitlesCol(rng.getFirstColumn(), rng.getLastColumn()); } } } } } } /** * Gets the hyperlinks on this sheet * * @return an array of hyperlinks */ public Hyperlink[] getHyperlinks() { Hyperlink[] hl = new Hyperlink[hyperlinks.size()]; for (int i = 0; i < hyperlinks.size(); i++) { hl[i] = (Hyperlink) hyperlinks.get(i); } return hl; } /** * Gets the cells which have been merged on this sheet * * @return an array of range objects */ public Range[] getMergedCells() { if (mergedCells == null) { return new Range[0]; } return mergedCells; } /** * Gets the non-default rows. Used when copying spreadsheets * * @return an array of row properties */ public RowRecord[] getRowProperties() { RowRecord[] rp = new RowRecord[rowProperties.size()]; for (int i = 0; i < rp.length; i++) { rp[i] = (RowRecord) rowProperties.get(i); } return rp; } /** * Gets the data validations. Used when copying sheets * * @return the data validations */ public DataValidation getDataValidation() { return dataValidation; } /** * Gets the row record. Usually called by the cell in the specified * row in order to determine its size * * @param r the row * @return the RowRecord for the specified row */ RowRecord getRowInfo(int r) { if (!rowRecordsInitialized) { rowRecords = new RowRecord[getRows()]; Iterator i = rowProperties.iterator(); int rownum = 0; RowRecord rr = null; while (i.hasNext()) { rr = (RowRecord) i.next(); rownum = rr.getRowNumber(); if (rownum < rowRecords.length) { rowRecords[rownum] = rr; } } rowRecordsInitialized = true; } return r < rowRecords.length ? rowRecords[r] : null; } /** * Gets the row breaks. Called when copying sheets * * @return the explicit row breaks */ public final int[] getRowPageBreaks() { return rowBreaks; } /** * Gets the row breaks. Called when copying sheets * * @return the explicit row breaks */ public final int[] getColumnPageBreaks() { return columnBreaks; } /** * Gets the charts. Called when copying sheets * * @return the charts on this page */ public final Chart[] getCharts() { Chart[] ch = new Chart[charts.size()]; for (int i = 0; i < ch.length; i++) { ch[i] = (Chart) charts.get(i); } return ch; } /** * Gets the drawings. Called when copying sheets * * @return the drawings on this page */ public final DrawingGroupObject[] getDrawings() { DrawingGroupObject[] dr = new DrawingGroupObject[drawings.size()]; dr = (DrawingGroupObject[]) drawings.toArray(dr); return dr; } /** * Determines whether the sheet is protected * * @return whether or not the sheet is protected * @deprecated in favour of the getSettings() api */ public boolean isProtected() { return settings.isProtected(); } /** * Gets the workspace options for this sheet. Called during the copy * process * * @return the workspace options */ public WorkspaceInformationRecord getWorkspaceOptions() { return workspaceOptions; } /** * Accessor for the sheet settings * * @return the settings for this sheet */ public SheetSettings getSettings() { return settings; } /** * Accessor for the workbook. In addition to be being used by this package, * it is also used during the importSheet process * * @return the workbook */ public WorkbookParser getWorkbook() { return workbook; } /** * Gets the column format for the specified column * * @param col the column number * @return the column format, or NULL if the column has no specific format * @deprecated use getColumnView instead */ public CellFormat getColumnFormat(int col) { CellView cv = getColumnView(col); return cv.getFormat(); } /** * Gets the column width for the specified column * * @param col the column number * @return the column width, or the default width if the column has no * specified format */ public int getColumnWidth(int col) { return getColumnView(col).getSize() / 256; } /** * Gets the column width for the specified column * * @param col the column number * @return the column format, or the default format if no override is specified */ public CellView getColumnView(int col) { ColumnInfoRecord cir = getColumnInfo(col); CellView cv = new CellView(); if (cir != null) { cv.setDimension(cir.getWidth() / 256); //deprecated cv.setSize(cir.getWidth()); cv.setHidden(cir.getHidden()); cv.setFormat(formattingRecords.getXFRecord(cir.getXFIndex())); } else { cv.setDimension(settings.getDefaultColumnWidth()); //deprecated cv.setSize(settings.getDefaultColumnWidth() * 256); } return cv; } /** * Gets the row height for the specified column * * @param row the row number * @return the row height, or the default height if the row has no * specified format * @deprecated use getRowView instead */ public int getRowHeight(int row) { return getRowView(row).getDimension(); } /** * Gets the row view for the specified row * * @param row the row number * @return the row format, or the default format if no override is specified */ public CellView getRowView(int row) { RowRecord rr = getRowInfo(row); CellView cv = new CellView(); if (rr != null) { cv.setDimension(rr.getRowHeight()); //deprecated cv.setSize(rr.getRowHeight()); cv.setHidden(rr.isCollapsed()); if (rr.hasDefaultFormat()) { cv.setFormat(formattingRecords.getXFRecord(rr.getXFIndex())); } } else { cv.setDimension(settings.getDefaultRowHeight()); cv.setSize(settings.getDefaultRowHeight()); //deprecated } return cv; } /** * Used when copying sheets in order to determine the type of this sheet * * @return the BOF Record */ public BOFRecord getSheetBof() { return sheetBof; } /** * Used when copying sheets in order to determine the type of the containing * workboook * * @return the workbook BOF Record */ public BOFRecord getWorkbookBof() { return workbookBof; } /** * Accessor for the environment specific print record, invoked when * copying sheets * * @return the environment specific print record */ public PLSRecord getPLS() { return plsRecord; } /** * Accessor for the button property set, used during copying * * @return the button property set */ public ButtonPropertySetRecord getButtonPropertySet() { return buttonPropertySet; } /** * Accessor for the number of images on the sheet * * @return the number of images on this sheet */ public int getNumberOfImages() { if (images == null) { initializeImages(); } return images.size(); } /** * Accessor for the image * * @param i the 0 based image number * @return the image at the specified position */ public Image getDrawing(int i) { if (images == null) { initializeImages(); } return (Image) images.get(i); } /** * Initializes the images */ private void initializeImages() { if (images != null) { return; } images = new ArrayList(); DrawingGroupObject[] dgos = getDrawings(); for (int i = 0; i < dgos.length; i++) { if (dgos[i] instanceof Drawing) { images.add(dgos[i]); } } } /** * Used by one of the demo programs for debugging purposes only */ public DrawingData getDrawingData() { SheetReader reader = new SheetReader(excelFile, sharedStrings, formattingRecords, sheetBof, workbookBof, nineteenFour, workbook, startPosition, this); reader.read(); return reader.getDrawingData(); } /** * Adds a local name to this shate * * @param nr the local name to add */ void addLocalName(NameRecord nr) { if (localNames == null) { localNames = new ArrayList(); } localNames.add(nr); } /** * Gets the conditional formats * * @return the conditional formats */ public ConditionalFormat[] getConditionalFormats() { ConditionalFormat[] formats = new ConditionalFormat[conditionalFormats.size()]; formats = (ConditionalFormat[]) conditionalFormats.toArray(formats); return formats; } /** * Returns the autofilter * * @return the autofilter */ public AutoFilter getAutoFilter() { return autoFilter; } /** * Accessor for the maximum column outline level. Used during a copy * * @return the maximum column outline level, or 0 if no outlines/groups */ public int getMaxColumnOutlineLevel() { return maxColumnOutlineLevel; } /** * Accessor for the maximum row outline level. Used during a copy * * @return the maximum row outline level, or 0 if no outlines/groups */ public int getMaxRowOutlineLevel() { return maxRowOutlineLevel; } }